#!/usr/bin/env escript
%% -*- erlang -*-

%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2020-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%

-mode(compile).
-compile(warnings_as_errors).

-import(lists, [foldl/3,sort/1]).

-record(st,
        {functions = [],
         types = [],
         callbacks = [],
         deprecations = #{}}).

main(["update",Top]) ->
    St0 = summarize(Top),
    St = check_deprecations(Top, St0),
    emit(Top, St),
    halt(0);
main(["make_markdown",Type,Top,Outfile]) ->
    St = summarize(Top),
    make_markdown(Top, Type, Outfile, St#st.functions),
    halt(0).

ebin_directories(Top) ->
    AppDirs0 = filelib:wildcard(filename:join(Top, "lib/*/ebin")),

    %% Filter out erl_interface and jinterface since they lack Erlang code, and
    %% ODBC because we can't build it on all platforms we develop on. This must
    %% be fixed before we deprecate or remove functionality in ODBC.
    AppDirs = [Dir || Dir <- AppDirs0,
                      not lists:suffix("erl_interface/ebin", Dir),
                      not lists:suffix("jinterface/ebin", Dir),
                      not lists:suffix("odbc/ebin", Dir)],

    [filename:join(Top, "erts/ebin")] ++ AppDirs.

summarize(Top) ->
    Directories = ebin_directories(Top),
    foldl(fun summarize_directory/2, #st{}, Directories).

summarize_directory(Dir, Acc) ->
    Files = [filename:join(Dir, F) || F <- filelib:wildcard("*.beam", Dir)],
    case Files of
        [_|_] ->
            foldl(fun summarize_file/2, Acc, Files);
        [] ->
            Msg = io_lib:format("~p doesn't appear to be built. Make sure to "
                                "build all OTP applications before updating "
                                "deprecations.\n", [Dir]),
            io:put_chars(standard_error, [Msg]),
            halt(1)
    end.

summarize_file(File, Acc) ->
    {ok, {Module, [Chunk]}} = beam_lib:chunks(File, [attributes]),
    {attributes, Attributes} = Chunk,
    summarize_attributes(Attributes, Module, Acc).

summarize_attributes([{deprecated, Ds} | As], Module, Acc0) ->
    Fs = sa_1(Ds, deprecated, Module, Acc0#st.functions),
    Acc = Acc0#st{ functions = Fs },
    summarize_attributes(As, Module, Acc);
summarize_attributes([{removed, Rs} | As], Module, Acc0) ->
    Fs = sa_1(Rs, removed, Module, Acc0#st.functions),
    Acc = Acc0#st{ functions = Fs },
    summarize_attributes(As, Module, Acc);
summarize_attributes([{deprecated_type, Ds} | As], Module, Acc0) ->
    Ts = sa_1(Ds, deprecated, Module, Acc0#st.types),
    Acc = Acc0#st{ types = Ts },
    summarize_attributes(As, Module, Acc);
summarize_attributes([{removed_type, Rs} | As], Module, Acc0) ->
    Ts = sa_1(Rs, removed, Module, Acc0#st.types),
    Acc = Acc0#st{ types = Ts },
    summarize_attributes(As, Module, Acc);
summarize_attributes([{deprecated_callback, Ds} | As], Module, Acc0) ->
    Ts = sa_1(Ds, deprecated, Module, Acc0#st.callbacks),
    Acc = Acc0#st{ callbacks = Ts },
    summarize_attributes(As, Module, Acc);
summarize_attributes([{removed_callback, Rs} | As], Module, Acc0) ->
    Ts = sa_1(Rs, removed, Module, Acc0#st.callbacks),
    Acc = Acc0#st{ callbacks = Ts },
    summarize_attributes(As, Module, Acc);
summarize_attributes([_ | As], Module, Acc) ->
    summarize_attributes(As, Module, Acc);
summarize_attributes([], _Module, Acc) ->
    Acc.

sa_1([{F, A, Info} | As], Tag, Module, Acc0) ->
    sa_1(As, Tag, Module, [{Tag, Module, F, A, Info} | Acc0]);
sa_1([{F, A} | As], Tag, Module, Acc0) ->
    sa_1(As, Tag, Module, [{Tag, Module, F, A, undefined} | Acc0]);
sa_1([module | As], Tag, Module, Acc0) ->
    sa_1(As, Tag, Module, [{Tag, Module, '_', '_', undefined} | Acc0]);
sa_1([], _Tag, _Module, Acc) ->
    Acc.

%%

emit(Top, #st{ functions = Fs0, types = Ts, callbacks = Cs, deprecations = Depr }) ->
    Fs = insert_removals(Fs0, Depr),
    Name = filename:join(Top, "lib/stdlib/src/otp_internal.erl"),
    Contents = [emit_header(),
                emit_function("obsolete", Fs),
                emit_function("obsolete_type", Ts),
                emit_function("obsolete_callback", Cs)],
    ok = file:write_file(Name, Contents),
    ok.

emit_header() ->
    {{YY, _, _}, {_, _, _}} = erlang:localtime(),
    io_lib:format(
      """
      %%
      %% %CopyrightBegin%
      %%
      %% SPDX-License-Identifier: Apache-2.0
      %%
      %% Copyright Ericsson AB 2020-~p. All Rights Reserved.
      %%
      %% Licensed under the Apache License, Version 2.0 (the "License");
      %% you may not use this file except in compliance with the License.
      %% You may obtain a copy of the License at
      %%
      %%     http://www.apache.org/licenses/LICENSE-2.0
      %%
      %% Unless required by applicable law or agreed to in writing, software
      %% distributed under the License is distributed on an "AS IS" BASIS,
      %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      %% See the License for the specific language governing permissions and
      %% limitations under the License.
      %%
      %% %CopyrightEnd%
      %%
      %% WARNING: DO NOT EDIT THIS FILE.
      %%
      %% This file was auto-generated from attributes in the source
      %% code.
      %%
      %% To add a description to a deprecation or removal attribute,
      %% write a string after the arity:
      %%
      %%    -deprecated([{foo,1,"use bar/1 instead"}]).
      %%    -deprecated_type([{gadget,1,"use widget/1 instead"}]).
      %%    -deprecated_callback([{gadget,1,"use widget/1 instead"}]).
      %%    -removed([{hello,2,"use there/2 instead"}]).
      %%    -removed_type([{frobnitz,1,"use grunka/1 instead"}]).
      %%    -removed_callback([{frobnitz,1,"use grunka/1 instead"}]).
      %%
      %% Descriptions cannot be given with the `f/1` shorthand, and
      %% it will fall back to a generic description referring the
      %% user to the documentation.
      %%
      %% Use `./otp_build update_deprecations` to update this file
      %% after adding an attribute.
      %%
      -module(otp_internal).
      -moduledoc false.
      -include("otp_internal.hrl").
      %%
      
      """,[YY]).

emit_function(FuncName, Entries) ->
    [io_lib:format("-dialyzer({no_match, ~ts/3}).\n", [FuncName]),
     [emit_clause(FuncName, E) || E <- sort_clauses(Entries)],
     io_lib:format("~ts(_,_,_) -> no.\n\n", [FuncName])].

sort_clauses(Entries) ->
    Tagged = [{clause_order(E), E} || E <- Entries],
    [E || {_, E} <- sort(Tagged)].

clause_order({_Tag, _Module, F, A, _Info}=Entry) ->
    {clause_order(F, A), Entry};
clause_order({_Tag, _Module, F, A, _Info, _Rel}) ->
    {clause_order(F, A), {_Tag, _Module, F, A, _Info}}.

%% Wildcard matches must be emitted *after* specific matches to avoid
%% losing descriptions.
clause_order(F, A) when F =/= '_', A =/= '_' -> 0;
clause_order(F, '_') when F =/= '_' -> 1;
clause_order('_', A) when A =/= '_' -> 2;
clause_order('_', '_') -> 3.

emit_clause(FuncName, {Tag, M, F, A, Info}) ->
    io_lib:format("~ts(~ts, ~ts, ~ts) ->\n"
                  "    {~p, ~p};\n",
                  [FuncName, match_string(M), match_string(F), match_string(A),
                   Tag, info_string(Info)]);
emit_clause(FuncName, {Tag, M, F, A, Info, Rel}) ->
    io_lib:format("~ts(~ts, ~ts, ~ts) ->\n"
                  "    {~p, ~p, ~p};\n",
                  [FuncName, match_string(M), match_string(F), match_string(A),
                   Tag, info_string(Info), Rel]).

%%

info_string(undefined) ->
    "see the documentation for details";
info_string(next_version) ->
    "will be removed in the next version. "
        "See the documentation for details";
info_string(next_major_release) ->
    "will be removed in the next major release. "
        "See the documentation for details";
info_string(eventually) ->
    "will be removed in a future release. "
        "See the documentation for details";
info_string(String) when is_list(String) ->
    String.

match_string('_') -> "_";
match_string(Term) -> io_lib:format("~p", [Term]).

%%

insert_removals([{deprecated,M,F,A,Info}=Entry|T], Depr) ->
    Key = {M,F,A},
    case Depr of
        #{Key := Ps} ->
            case lists:keyfind(remove, 1, Ps) of
                false ->
                    [Entry|insert_removals(T, Depr)];
                {remove,Rel0} ->
                    Rel = lists:concat(["OTP ",Rel0]),
                    [{deprecated,M,F,A,Info,Rel}|insert_removals(T, Depr)]
            end;
        #{} ->
            [Entry|insert_removals(T, Depr)]
    end;
insert_removals([H|T], Depr) ->
    [H|insert_removals(T, Depr)];
insert_removals([], _Depr) ->
    [].

%%%
%%% Create XML files.
%%%

make_markdown(Top, Type, OutFile, InfoText0) ->
    DeprecationFile = deprecation_file(Top),
    OutDir = filename:dirname(DeprecationFile),
    Depr0 = read_deprecations(DeprecationFile),
    Depr = maps:to_list(Depr0),

    {RelKey, AttrTag} =
        case Type of
            "deprecations" ->
                %% Group by 'since' in DEPRECATIONS, grab text from
                %% 'deprecated' attributes.
                {since, deprecated};
            "scheduled_for_removal" ->
                {remove, deprecated};
            "removed" ->
                {remove, removed}
        end,

    InfoTextMap = maps:from_list(make_markdown_info(InfoText0, AttrTag)),
    CollectedMFAs = make_markdown_collect(Depr, RelKey, InfoTextMap, []),

    case Type of
        "removed" ->
            {ok, Vsn} = file:read_file(filename:join(Top,"OTP_VERSION")),
            [Release|_] = string:split(Vsn,"."),
            lists:foreach(
              fun({Rel, Functions}) ->
                      case Rel > binary_to_integer(Release) of
                          true ->
                              io:format(standard_error,
                                        "Some functions have been marked "
                                        "as removed in the future: ~n~p~n",
                                        [Functions]),
                              halt(1);
                          false ->
                              ok
                      end
              end, CollectedMFAs);
        _ ->
            ok
    end,
    TemplCollected = collect_template_rels(OutDir, Type),
    MergedCollected = merge_collected(TemplCollected, CollectedMFAs),
    All = make_markdown_gen(lists:reverse(MergedCollected), Type, OutDir),
    ok = file:write_file(OutFile, All).

make_markdown_info([{Tag,M,F,A,Text} | Attributes], Tag) ->
    [{{M,F,A}, info_string(Text)} | make_markdown_info(Attributes, Tag)];
make_markdown_info([_ | Attributes], Tag) ->
    make_markdown_info(Attributes, Tag);
make_markdown_info([], _Tag) ->
    [].

%% Joins `DEPRECATIONS` with module attributes, grabbing the text from said
%% attributes and grouping them by the release version pointed out by `RelKey`
%% ('since' or 'remove').
make_markdown_collect([{MFA, Ps} | T], RelKey, InfoTextMap, Acc0) ->
    Acc = case lists:keyfind(RelKey, 1, Ps) of
              {RelKey, Rel} ->
                  case InfoTextMap of
                      #{ MFA := Text } ->
                          [{Rel, {MFA,Text}} | Acc0];
                      #{} ->
                          Acc0
                  end;
              false ->
                  Acc0
          end,
    make_markdown_collect(T, RelKey, InfoTextMap, Acc);
make_markdown_collect([], _RelKey, _InfoTextMap, Acc) ->
    rel2fam(Acc).

make_markdown_gen(Collected, Type, Dir) ->
    Head = get_template(Dir, Type, head),
    Contents = make_gen_list(Collected, Type, Dir),
    [string:trim(Head),"\n\n",Contents].

make_gen_list([{Rel,MFAs}|T], Type, Dir) ->
    RelStr = lists:concat(["OTP ",Rel]),
    Head = ["## ",RelStr,"\n\n"],
    [string:trim([Head, get_template(Dir, Type, Rel)]), "\n\n",
     [if MFAs == [] ->
              [];
         true ->
              SubTitle = case Type of
                             "deprecations" ->
                                 ["Functions Deprecated in ",RelStr];
                             "scheduled_for_removal" ->
                                 ["Functions Scheduled for Removal in ",RelStr];
                             "removed" ->
                                 ["Functions Removed in ",RelStr]
                         end,
              SubHead = ["### ",SubTitle,"\n\n"],
              [string:trim([SubHead, make_gen_mfas(MFAs)]), "\n\n"]
      end] |
     make_gen_list(T, Type, Dir)];
make_gen_list([], _, _) ->
    [].

make_gen_mfas(MFAs) ->
     [make_item(MFA) || MFA <- MFAs].

make_item({{lists,zf,2},Text}) ->
    %% Avoid creating a reference to this undocumented function.
    ["-   Undocumented function lists:zf/2 ", " (",Text,")\n"];
make_item({{M,F,A},Text}) ->
    ["-   `",lists:concat([M,":",F,"/",A]),"` ", " (",Text,")\n"].

get_template(Dir, Prefix, Key) ->
    Name = lists:concat([Prefix,"_",Key,".md"]),
    File = filename:join(Dir, Name),
    case file:read_file(File) of
        {ok,Contents} ->
            Contents;
        {error,enoent} ->
            []
    end.

collect_template_rels(Dir, Prefix) ->
    FileNamePrefix = Prefix++"_",
    {ok, FileNames} = file:list_dir(Dir),
    TemplRels = lists:foldl(fun (FileName, Acc) ->
                                    try
                                        Suffix = string:prefix(FileName,
                                                               FileNamePrefix),
                                        [RelStr, "md"] = string:split(Suffix,
                                                                      "."),
                                        Rel = list_to_integer(RelStr),
                                        [{Rel, []}|Acc]
                                    catch
                                        _ : _ ->
                                            Acc
                                    end
                            end,
                            [],
                            FileNames),
    lists:keysort(1, TemplRels).

merge_collected([{Rel, []} | WithoutMFAs], [{Rel, _} = Use | WithMFAs]) ->
    [Use | merge_collected(WithoutMFAs, WithMFAs)];
merge_collected([{RelA, []} = Use | WithoutMFAs],
                [{RelB, _} | _] = WithMFAs) when RelA < RelB ->
    [Use | merge_collected(WithoutMFAs, WithMFAs)];
merge_collected([{_RelA, []} | _] = WithoutMFAs,
                [{_RelB, _} = Use | WithMFAs]) ->
    [Use | merge_collected(WithoutMFAs, WithMFAs)];
merge_collected([], WithMFAs) ->
    WithMFAs;
merge_collected(WithoutMFAs, []) ->
    WithoutMFAs.

%%%
%%% Cross-checks deprecations against DEPRECATIONS file.
%%%

check_deprecations(Top, #st{functions = Fs} = St) ->
    DeprFile = deprecation_file(Top),
    Depr = read_deprecations(DeprFile),
    Bad0 = [F || F <- Fs, not in_deprecations(F, Depr)],
    case Bad0 of
        [] ->
            St#st{deprecations = Depr};
        [_|_] ->
            Msg = "The following function(s) have -deprecated() or "
                "-removed() attributes, but are not present in the "
                "DEPRECATIONS file:\n\n",
            Bad = [io_lib:format("  ~w:~w/~w\n", [M,F,A]) ||
                      {_,M,F,A,_} <- Bad0],
            Loc = ["\n","Please update ",DeprFile,".\n"],
            io:put_chars(standard_error, [Msg,Bad,Loc]),
            halt(1)
    end.

read_deprecations(File) ->
    {ok,Bin} = file:read_file(File),
    Lines = binary:split(Bin, <<"\n">>, [global,trim_all]),
    maps:from_list(parse_deprecations(Lines)).

deprecation_file(Root) ->
    filename:join(Root, "system/doc/general_info/DEPRECATIONS").

in_deprecations({Tag,M,F,A,_}, Depr) when Tag =:= deprecated; Tag =:= removed ->
    is_map_key({M,F,A}, Depr).

parse_deprecations([<<"#",_/binary>>|Lines]) ->
    parse_deprecations(Lines);
parse_deprecations([Line|Lines]) ->
    [parse_line(Line)|parse_deprecations(Lines)];
parse_deprecations([]) ->
    [].

parse_line(Line) ->
    [MFA0|Parts0] = binary:split(Line, <<" ">>, [global,trim_all]),
    MFA = parse_mfa(MFA0),
    Parts1 = [binary:split(Part, <<"=">>) || Part <- Parts0],
    Parts = lists:sort([parse_part(Part) || Part <- Parts1]),
    {MFA,Parts}.

parse_part([<<"mfa">>,MFA]) ->
    {mfa,parse_mfa(MFA)};
parse_part([<<"since">>,Since]) ->
    {since,parse_release(Since)};
parse_part([<<"remove">>,Remove]) ->
    {remove,parse_release(Remove)}.

parse_release(Rel) ->
    binary_to_integer(Rel).

parse_mfa(MFA) ->
    {match,[M0,F0,A0]} = re:run(MFA, <<"^(\\w+):(\\w+)/([\\d_]+)$">>,
                                [{capture,all_but_first,binary}]),
    A = case A0 of
            <<"_">> -> '_';
            _ -> binary_to_integer(A0)
        end,
    {bin_to_atom(M0),bin_to_atom(F0),A}.

bin_to_atom(Bin) ->
    list_to_atom(binary_to_list(Bin)).

rel2fam(S0) ->
    S1 = sofs:relation(S0),
    S = sofs:rel2fam(S1),
    sofs:to_external(S).
